/*
 * Copyright 2022 Apollo Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
package com.ctrip.framework.apollo.portal.spi.ldap;

import com.ctrip.framework.apollo.portal.entity.bo.UserInfo;
import com.ctrip.framework.apollo.portal.spi.UserService;
import com.ctrip.framework.apollo.portal.spi.configuration.LdapExtendProperties;
import com.ctrip.framework.apollo.portal.spi.configuration.LdapProperties;
import com.google.common.base.Strings;
import com.google.common.collect.Sets;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.ldap.core.AttributesMapper;
import org.springframework.ldap.core.ContextMapper;
import org.springframework.ldap.core.DirContextAdapter;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.query.ContainerCriteria;
import org.springframework.ldap.query.SearchScope;
import org.springframework.ldap.support.LdapUtils;
import org.springframework.util.CollectionUtils;

import javax.naming.directory.Attribute;
import javax.naming.ldap.LdapName;
import java.util.*;

import static java.util.stream.Collectors.collectingAndThen;
import static java.util.stream.Collectors.toCollection;
import static org.springframework.ldap.query.LdapQueryBuilder.query;

/**
 * Ldap user spi service
 * <p>
 * Support OpenLdap,ApacheDS,ActiveDirectory use {@link LdapTemplate} as underlying implementation
 *
 * @author xm.lin xm.lin@anxincloud.com
 * @author idefav
 * @Description ldap user service
 * @date 18-8-9 下午4:42
 */
public class LdapUserService implements UserService {

    @Autowired
    private LdapProperties ldapProperties;

    @Autowired
    private LdapExtendProperties ldapExtendProperties;

    /**
     * ldap search base
     */
    @Value("${spring.ldap.base}")
    private String base;

    /**
     * user objectClass
     */
    @Value("${ldap.mapping.objectClass}")
    private String objectClassAttrName;

    /**
     * user LoginId
     */
    @Value("${ldap.mapping.loginId}")
    private String loginIdAttrName;

    /**
     * user displayName
     */
    @Value("${ldap.mapping.userDisplayName}")
    private String userDisplayNameAttrName;

    /**
     * email
     */
    @Value("${ldap.mapping.email}")
    private String emailAttrName;

    /**
     * rdn
     */
    @Value("${ldap.mapping.rdnKey:}")
    private String rdnKey;

    /**
     * memberOf
     */
    @Value("#{'${ldap.filter.memberOf:}'.split('\\|')}")
    private String[] memberOf;

    /**
     * group search base
     */
    @Value("${ldap.group.groupBase:}")
    private String groupBase;

    /**
     * group filter eg. (&(cn=apollo-admins)(&(member=*)))
     */
    @Value("${ldap.group.groupSearch:}")
    private String groupSearch;

    /**
     * group memberShip eg. member
     */
    @Value("${ldap.group.groupMembership:}")
    private String groupMembershipAttrName;


    @Autowired
    private LdapTemplate ldapTemplate;

    private static final String MEMBER_OF_ATTR_NAME = "memberOf";
    private static final String MEMBER_UID_ATTR_NAME = "memberUid";

    /**
     * 用户信息Mapper
     */
    private final ContextMapper<UserInfo> ldapUserInfoMapper = (ctx) -> {
        DirContextAdapter contextAdapter = (DirContextAdapter) ctx;
        UserInfo userInfo = new UserInfo();
        userInfo.setUserId(contextAdapter.getStringAttribute(loginIdAttrName));
        userInfo.setName(contextAdapter.getStringAttribute(userDisplayNameAttrName));
        userInfo.setEmail(contextAdapter.getStringAttribute(emailAttrName));
        return userInfo;
    };

    /**
     * 查询条件
     */
    private ContainerCriteria ldapQueryCriteria() {
        ContainerCriteria criteria = query()
                .searchScope(SearchScope.SUBTREE)
                .where("objectClass").is(objectClassAttrName);
        if (memberOf.length > 0 && !StringUtils.isEmpty(memberOf[0])) {
            ContainerCriteria memberOfFilters = query().where(MEMBER_OF_ATTR_NAME).is(memberOf[0]);
            Arrays.stream(memberOf).skip(1)
                    .forEach(filter -> memberOfFilters.or(MEMBER_OF_ATTR_NAME).is(filter));
            criteria.and(memberOfFilters);
        }
        return criteria;
    }

    /**
     * 根据entryDN查找用户信息
     *
     * @param member  ldap EntryDN
     * @param userIds 用户ID列表
     */
    private UserInfo lookupUser(String member, List<String> userIds) {
        return ldapTemplate.lookup(member, (AttributesMapper<UserInfo>) attributes -> {
            UserInfo tmp = new UserInfo();
            Attribute emailAttribute = attributes.get(emailAttrName);
            if (emailAttribute != null && emailAttribute.get() != null) {
                tmp.setEmail(emailAttribute.get().toString());
            }
            Attribute loginIdAttribute = attributes.get(loginIdAttrName);
            if (loginIdAttribute != null && loginIdAttribute.get() != null) {
                tmp.setUserId(loginIdAttribute.get().toString());
            }
            Attribute userDisplayNameAttribute = attributes.get(userDisplayNameAttrName);
            if (userDisplayNameAttribute != null && userDisplayNameAttribute.get() != null) {
                tmp.setName(userDisplayNameAttribute.get().toString());
            }

            if (userIds != null) {
                if (userIds.stream().anyMatch(c -> c.equals(tmp.getUserId()))) {
                    return tmp;
                }
                return null;
            }
            return tmp;

        });
    }

    private UserInfo searchUserById(String userId) {
        try {
            return ldapTemplate.searchForObject(query().where(loginIdAttrName).is(userId),
                    ctx -> {
                        UserInfo userInfo = new UserInfo();
                        DirContextAdapter contextAdapter = (DirContextAdapter) ctx;
                        userInfo.setEmail(contextAdapter.getStringAttribute(emailAttrName));
                        userInfo.setName(contextAdapter.getStringAttribute(userDisplayNameAttrName));
                        userInfo.setUserId(contextAdapter.getStringAttribute(loginIdAttrName));
                        return userInfo;
                    });
        } catch (EmptyResultDataAccessException ex) {
            // EmptyResultDataAccessException means no record found
            return null;
        }
    }

    /**
     * 按照group搜索用户
     *
     * @param groupBase   group search base
     * @param groupSearch group filter
     * @param keyword     user search keywords
     * @param userIds     user id list
     */
    private List<UserInfo> searchUserInfoByGroup(String groupBase, String groupSearch,
                                                 String keyword, List<String> userIds) {

        return ldapTemplate
                .searchForObject(groupBase, groupSearch, ctx -> {
                    List<UserInfo> userInfos = new ArrayList<>();

                    if (!MEMBER_UID_ATTR_NAME.equals(groupMembershipAttrName)) {
                        String[] members = ((DirContextAdapter) ctx).getStringAttributes(groupMembershipAttrName);
                        for (String item : members) {
                            LdapName ldapName = LdapUtils.newLdapName(item);
                            LdapName memberRdn = LdapUtils.removeFirst(ldapName, LdapUtils.newLdapName(base));
                            if (keyword != null) {
                                String rdnValue = LdapUtils.getValue(memberRdn, rdnKey).toString();
                                if (rdnValue.toLowerCase().contains(keyword.toLowerCase())) {
                                    UserInfo userInfo = lookupUser(memberRdn.toString(), userIds);
                                    userInfos.add(userInfo);
                                }
                            } else {
                                UserInfo userInfo = lookupUser(memberRdn.toString(), userIds);
                                if (userInfo != null) {
                                    userInfos.add(userInfo);
                                }
                            }

                        }
                        return userInfos;
                    }

                    Set<String> memberUids = Sets.newHashSet(((DirContextAdapter) ctx)
                            .getStringAttributes(groupMembershipAttrName));
                    if (!CollectionUtils.isEmpty(userIds)) {
                        memberUids = Sets.intersection(memberUids, Sets.newHashSet(userIds));
                    }
                    for (String memberUid : memberUids) {
                        UserInfo userInfo = searchUserById(memberUid);
                        if (userInfo != null) {
                            if (keyword != null) {
                                if (userInfo.getUserId().toLowerCase().contains(keyword.toLowerCase())) {
                                    userInfos.add(userInfo);
                                }
                            } else {
                                userInfos.add(userInfo);
                            }
                        }
                    }
                    return userInfos;
                });
    }

    @Override
    public List<UserInfo> searchUsers(String keyword, int offset, int limit, boolean includeInactiveUsers) {
        List<UserInfo> users = new ArrayList<>();
        if (StringUtils.isNotBlank(groupSearch)) {
            List<UserInfo> userListByGroup = searchUserInfoByGroup(groupBase, groupSearch, keyword,
                    null);
            users.addAll(userListByGroup);
            return users.stream().collect(collectingAndThen(toCollection(() -> new TreeSet<>((o1, o2) -> {
                if (o1.getUserId().equals(o2.getUserId())) {
                    return 0;
                }
                return -1;
            })), ArrayList::new));
        }
        ContainerCriteria criteria = ldapQueryCriteria();
        if (!Strings.isNullOrEmpty(keyword)) {
            criteria.and(query().where(loginIdAttrName).like(keyword + "*").or(userDisplayNameAttrName)
                    .like(keyword + "*"));
        }
        users = ldapTemplate.search(criteria, ldapUserInfoMapper);
        return users;
    }

    @Override
    public UserInfo findByUserId(String userId) {
        if (StringUtils.isNotBlank(groupSearch)) {
            List<UserInfo> lists = searchUserInfoByGroup(groupBase, groupSearch, null,
                    Collections.singletonList(userId));
            if (lists != null && !lists.isEmpty() && lists.get(0) != null) {
                return lists.get(0);
            }
            return null;
        }

        try {
            return ldapTemplate
                    .searchForObject(ldapQueryCriteria().and(loginIdAttrName).is(userId), ldapUserInfoMapper);
        } catch (EmptyResultDataAccessException ex) {
            // EmptyResultDataAccessException means no record found
            return null;
        }
    }

    @Override
    public List<UserInfo> findByUserIds(List<String> userIds) {
        if (CollectionUtils.isEmpty(userIds)) {
            return Collections.emptyList();
        }
        if (StringUtils.isNotBlank(groupSearch)) {
            return searchUserInfoByGroup(groupBase, groupSearch, null, userIds);
        }
        ContainerCriteria criteria = query().where(loginIdAttrName).is(userIds.get(0));
        userIds.stream().skip(1).forEach(userId -> criteria.or(loginIdAttrName).is(userId));
        return ldapTemplate.search(ldapQueryCriteria().and(criteria), ldapUserInfoMapper);
    }

}
